<?php
/**
 * 各种指标（MA,EMA,DEA,MACD）、买卖点、收益率、收益率排序，的计算，
 * Created by PhpStorm.
 * User: mUsr
 * Date: 18/6/27
 * Time: 下午5:03
 */


/**
 * 计算移动平均值（MA），绝对平均
 * @param $data 每日K线数据包，日期和收盘价
 * @param $MA_tmp 上次计算的临时数据
 * @return Generator 输出每天的全部类型的日均线值
 */
function generate_MA($data, $MA_tmp = null)
{
    // 如果有上次的临时数据，则作为当前的临时数据，否则重新开始
    if ($MA_tmp) {
        $t = $MA_tmp;
    } else {
        $t = array(); // 存放日均线的，前n天的，每日的收盘价
        for ($k = 1; $k <= 20; $k++) {
            $t[$k] = array(); // 每种类型日均线的临时数据
        }
    }

    // 循环每天的收盘价
    foreach ($data as $value) {
        // 输出数据
        $out = array();
        $out[0] = $value['date'];

        // 循环日均线类型
        for ($j = 1; $j <= 20; $j++) {

            array_push($t[$j], $value['close']); // 把当天的收盘价压入临时数组

            // 保持数组中个数
            while (count($t[$j]) > $j) {
                array_shift($t[$j]);
            }

            // 求平均值
            $avg = round(array_sum($t[$j]) / count($t[$j]), 3);
            $out[$j] = $avg;
        }

        yield ['out' => $out, 'MA_tmp' => $t]; // 计算完成一条完整的记录后输出，并带出临时数据
    }
}


/**
 * 计算移动平均值（EMA）加权平均，长短EMA，供计算DEA，MACD用
 * @param $data 日K线数据包，日期和收盘价
 * @param $type 日均线类型
 * @param $last 上次计算的最后一个输出
 * @return Generator 输出每天的全部类型的加权日均线值
 */
function generate_EMA($data, $type, $last = null)
{
    // 12日EMA的算式为
    // EMA（12）=前一日EMA（12）×11/13+今日收盘价×2/13
    // 26日EMA的算式为
    // EMA（26）=前一日EMA（26）×25/27+今日收盘价×2/27

    // 输出数据，上次计算的最后一个输出作为上一日的数据，如果没有上次的结果，则重新初始化
    if ($last) {
        $out = $last;
    } else {
        $out = array();
    }

    // 循环每天的收盘价
    foreach ($data as $value) {

        $out[0] = $value['date'];

        // 循环日均线类型
        foreach ($type as $val) {
            // 初始化，如果没有前一日的EMA，就设置为当天的收盘价
            $out[$val] = isset($out[$val]) ? $out[$val] : $value['close'];
            // 计算当日EMA
            $out[$val] = $out[$val] * ($val - 1) / ($val + 1) + $value['close'] * 2 / ($val + 1);
            $out[$val] = round($out[$val], 3);
        }

        yield $out; // 计算完成一条完整的记录后输出
    }
}


/**
 * DEA，MACD值
 * @param $data 长短EMA
 * @param $type 最多计算多少天，超过20天要增加EMA2的生成数据
 * @param $last 上次计算的最后一个输出
 * @return Generator
 */
function generate_DEA_MACD($data, $type = 1, $last = null)
{
    // 2、计算离差值（DIF）
    // DIF=今日EMA（12）－今日EMA（26）
    // 3、计算DIF的9日EMA
    // 根据离差值计算其9日的EMA，即离差平均值，是所求的MACD值。为了不与指标原名相混淆，此值又名
    // DEA或DEM。
    // 今日DEA（MACD）=前一日DEA×8/10+今日DIF×2/10计算出的DIF和DEA的数值均为正值或负值。
    // 用（DIF-DEA）×2即为MACD柱状图。

    // 输出数据，上次计算的最后一个输出作为上一日的数据，如果没有上次的结果，则重新初始化
    if ($last) {
        $out = $last;
    } else {
        $out = array();
    }

    // 循环每天的DIF
    foreach ($data as $value) {

        $out['date'] = $value['date'];

        for ($i = 1; $i <= $type; $i++) {
            // 参数
            $short_ema = 12 * $i; // 短移动平均值（EMA）的天数
            $long_ema = 26 * $i; // 长移动平均值（EMA）的天数
            $dif_ema = 9 * $i; // 计算DIF的EMA的天数

            if ($value["ema_{$short_ema}"] && $value["ema_{$long_ema}"]) { // 排除空数据

                // 初始化，如果没有前一日的DEA，就设置为当天的DIF
                $out["dea_{$i}"] = isset($out["dea_{$i}"]) ? $out["dea_{$i}"] : $value["ema_{$short_ema}"] - $value["ema_{$long_ema}"];

                // 计算当日的DEA，默认值：前一日DEA×8/10+今日DIF×2/10
                $out["dea_{$i}"] = $out["dea_{$i}"] * ($dif_ema - 1) / ($dif_ema + 1) + ($value["ema_{$short_ema}"] - $value["ema_{$long_ema}"]) * 2 / ($dif_ema + 1); // DEA

                // 计算MACD柱状图值，用（DIF-DEA）×2即为MACD柱状图
                $out["macd_{$i}"] = ($value["ema_{$short_ema}"] - $value["ema_{$long_ema}"] - $out["dea_{$i}"]) * 2; // MACD柱

                // 格式小数位数
                $out["dea_{$i}"] = round($out["dea_{$i}"], 3);
                $out["macd_{$i}"] = round($out["macd_{$i}"], 3);
            }
        }
        yield $out; // 计算完成后输出
    }
}


/**
 * 生成或更新MA数据
 * @param $mongo mongodb连接实例
 * @param $code 股票代码
 * @param $data 行情数据
 * @param $rt_type 实时类型，1历史行情，2实时行情
 */
function create_MA($mongo, $code, $data, $rt_type = 1)
{
    // 集合MA保存历史数据，MA2保存实时数据，实时数据只保存最近一条
    // 所有计算，都以历史数据的上次计算的临时数据，和当前的数据（可能历史，也可能为实时）为输入

    $ma = array(); // 计算结果
    $MA_tmp = array(); // 计算平均值的临时数据
    $collection = $mongo->stock->MA;

    // 读取上次计算的结果和临时数据
    $document = $collection->findOne(['_id' => $code]);
    if ($document) {
        // 上次的临时数据
        $i = 1; // 下标从1开始
        // 转换对象为数组
        foreach ($document['MA_tmp'] as $value) {
            $tmp = array();
            // 转换对象为数组
            foreach ($value as $key => $val) {
                $tmp[$key] = $val;
            }
            $MA_tmp[$i] = $tmp;
            $i++;
        }
    }

    // 计算MA
    $generate = generate_MA($data, $MA_tmp);
    foreach ($generate as $item) {
        $value = $item['out'];
        $count = count($value);
        $temp = array();
        $temp['date'] = $value[0]; // 日期

        if ($count >= 2) { // 计算结果中至少应该有2个字段，日期和ma1
            for ($i = 1; $i < $count; $i++) {
                $temp["ma_{$i}"] = $value[$i];
            }
            $ma[] = $temp;
        }
    }

    if ($rt_type == 1) {
        // 历史行情
        $collection->deleteOne(['_id' => $code]); // 删除旧的数据
        $re = [['_id' => $code, 'MA' => $ma, 'MA_tmp' => $item['MA_tmp']]]; // 打包成一个对象数组

    } elseif ($rt_type == 2) {
        // 实时行情
        $collection = $mongo->stock->MA2;
        $collection->deleteOne(['_id' => $code]); // 删除旧的数据
        $re = [['_id' => $code, 'MA' => $ma]]; // 打包成一个对象数组

    } else {
        return;
    }
    $collection->insertMany($re); // 写入mongoDB

    return true;
}


/**
 * 生成或更新EMA数据
 * @param $mongo mongodb连接实例
 * @param $code 股票代码
 * @param $data 行情数据
 * @param $rt_type 实时类型，1历史行情，2实时行情
 */
function create_EMA($mongo, $code, $data, $rt_type = 1)
{
    // 集合EMA保存历史数据，EMA2保存实时数据，实时数据只保存最近一条
    // 所有计算，都以历史数据的上次计算结果，和当前的数据（可能历史，也可能为实时）为输入

    $ema = array(); // 计算结果
    $last = array(); // 上次计算的最后一个输出
    $collection = $mongo->stock->EMA;

    // 读取上次计算的结果和最后一个输出
    $document = $collection->findOne(['_id' => $code]);
    if ($document) {
        // 上次的最后一个输出
        // 转换对象为数组
        foreach ($document['last'] as $key => $value) {
            $last[$key] = $value;
        }
    }

    // 生成类型数据
    $type = array();
    for ($i = 1; $i <= 20; $i++) {
        $type[] = $i * 9;
        $type[] = $i * 12;
        $type[] = $i * 26;
    }
    $type = array_unique($type); // 去重
    sort($type); // 排序

    $generate = generate_EMA($data, $type, $last);
    foreach ($generate as $value) {
        $temp = array();
        $temp['date'] = $value[0]; // 日期
        $count = count($value);
        if ($count >= 2) {
            foreach ($type as $v) {
                $temp["ema_$v"] = $value[$v];
            }
            $ema[] = $temp;
        }
    }

    if ($rt_type == 1) {
        // 历史行情
        $collection->deleteOne(['_id' => $code]); // 删除旧的数据
        $re = [['_id' => $code, 'EMA' => $ema, 'last' => $value]];

    } elseif ($rt_type == 2) {
        // 实时行情
        $collection = $mongo->stock->EMA2;
        $collection->deleteOne(['_id' => $code]); // 删除旧的数据
        $re = [['_id' => $code, 'EMA' => $ema]];

    } else {
        return;
    }
    $collection->insertMany($re);

    return true;
}


/**
 * 生成DEA_MACD
 * @param $mongo mongodb连接实例
 * @param $code 股票代码
 * @param $rt_type 实时类型，1历史行情，2实时行情
 */
function create_DEA_MACD($mongo, $code, $rt_type = 1)
{
    // 集合MACD保存历史数据，MACD2保存实时数据，实时数据只保存最近一条
    // 所有计算，都以历史数据的上次计算结果，和当前的数据（可能历史，也可能为实时）为输入

    $macd = array(); // 计算结果
    $last = array(); // 上次计算的最后一个输出
    $collection = $mongo->stock->MACD;

    // 读取上次计算的结果和最后一个输出
    $document = $collection->findOne(['_id' => $code]);
    if ($document) {
        // 上次的最后一个输出
        // 转换对象为数组
        foreach ($document['last'] as $key => $value) {
            $last[$key] = $value;
        }
    }

    // 读取数据
    if ($rt_type == 1) {
        // 历史行情
        $collect = "EMA";

    } elseif ($rt_type == 2) {
        // 实时行情
        $collect = "EMA2";

    } else {
        return;
    }
    $collection_ema = $mongo->stock->$collect;
    $cursor = $collection_ema->find(['_id' => $code]);
    foreach ($cursor as $document) {
        $data = array();
        foreach ($document['EMA'] as $value) {
            $tmp = array();
            foreach ($value as $key => $val) {
                $tmp[$key] = $val;
            }
            $data[] = $tmp;
        }
    }

    // 开始生成
    $generate = generate_DEA_MACD($data, 20, $last);
    foreach ($generate as $key => $value) {
        $temp = array();
        foreach ($value as $k => $v) {
            $temp[$k] = $v;
        }
        $macd[] = $temp;
    }

    if ($rt_type == 1) {
        // 历史行情
        $collection->deleteOne(['_id' => $code]); // 删除旧的数据
        $re = [['_id' => $code, 'MACD' => $macd, 'last' => $value]];

    } elseif ($rt_type == 2) {
        // 实时行情
        $collection = $mongo->stock->MACD2;
        $collection->deleteOne(['_id' => $code]); // 删除旧的数据
        $re = [['_id' => $code, 'MACD' => $macd]];

    } else {
        return;
    }
    $collection->insertMany($re);

    return true;
}


/**
 * 读取MA,MACD
 * @param $mongo mongodb连接实例
 * @param $code 股票代码
 * @param $rt_type 实时类型，1历史行情，2实时行情
 * @return array 重新组织后的MA MACD
 */
function read_MA_MACD($mongo, $code, $rt_type = 1)
{
    // 读取MA数据
    if ($rt_type == 1) {
        // 历史行情
        $collect = "MA";

    } elseif ($rt_type == 2) {
        // 实时行情
        $collect = "MA2";

    } else {
        return;
    }
    $collection = $mongo->stock->$collect;
    $document = $collection->findOne(['_id' => $code]);
    $ma = array();
    // 转换对象为数组
    foreach ($document['MA'] as $value) {
        $tmp = array();
        // 转换对象为数组
        foreach ($value as $key => $val) {
            $tmp[$key] = $val;
        }
        $ma[] = $tmp;
    }

    // 读取MACD数据，并合并MA todo 验证日期是否一致
    if ($rt_type == 1) {
        // 历史行情
        $collect = "MACD";

    } elseif ($rt_type == 2) {
        // 实时行情
        $collect = "MACD2";
    }
    $collection = $mongo->stock->$collect;
    $document = $collection->findOne(['_id' => $code]);
    $i = 0;
    $data = array();
    // 转换对象为数组
    foreach ($document['MACD'] as $value) {
        $tmp = $ma[$i]; // 准备合并数据，添加某一天的所有MA
        // 转换对象为数组
        foreach ($value as $key => $val) {
            if (strstr($key, 'macd_')) {
                $tmp[$key] = $val; // 循环添加某一天的MACD
            }
        }
        $data[] = $tmp;
        $i += 1;
    }

    return $data;
}


/**
 * 计算买卖点
 * @param $where 条件组合参数
 * @param $value 当前的日均线MA和MACD
 * @param $stat 买入卖出状态，用于判断当前计算为买入点，或卖出点，买入后保存的是买入价格
 */
function calculate_buysell($where, $value, $stat = null)
{
    $out = array(); // 输出内容

    // 买入条件，Macd≥-0.0x and x日Macd≥-0.0x and 短均线取值/长均线取值≥1.00x，（因浮点数不能直接比较相等，改为判断不小于）
    if (!$stat && $where['macd'] > 1) {
        $where_buy = (!$stat && !($value["macd_1"] < $where['x_macd'] / 100)
            && !($value["macd_{$where['macd']}"] < $where['x_macd'] / 100)
            && !($value["ma_{$where['short']}"] / $value["ma_{$where['long']}"] < 1 + $where['x_ma'] / 1000));
    } else {
        $where_buy = (!$stat && !($value["macd_1"] < $where['x_macd'] / 100)
            && !($value["ma_{$where['short']}"] / $value["ma_{$where['long']}"] < 1 + $where['x_ma'] / 1000));
    }

    if ($where_buy) {
        // 输出内容
        $out = ['date' => $value['date'], 'buy_sell' => 1, 'price' => $value['ma_1'], 'short' => $value["ma_{$where['short']}"],
            'long' => $value["ma_{$where['long']}"], 'macd' => $value["macd_{$where['macd']}"]]; // 买入日期，和买入价格，日均线，MACD
        $stat = $value['ma_1']; // 状态为买入，保存买入价格
    }

    // 卖出条件，长均线取值/短均线取值≥1.00x，（因浮点数不能直接比较相等，改为判断不小于）
    if ($stat && !($value["ma_{$where['long']}"] / $value["ma_{$where['short']}"] < 1 + $where['x_ma2'] / 1000)) {

        // 收益金额
        $transactProfit = $value['ma_1'] - $stat;
        // 收益比例
        $transactRate = $transactProfit / $stat;

        // 输出内容
        $out = ['date' => $value['date'], 'buy_sell' => 2, 'price' => $value['ma_1'], 'transactProfit' => $transactProfit, 'transactRate' => $transactRate,
            'short' => $value["ma_{$where['short']}"], 'long' => $value["ma_{$where['long']}"]]; // 卖出日期，卖出价格，和收益，日均线
        $stat = null; // 状态为卖出，删除保存的价格
    }

    if ($out) {
        return ['out' => $out, 'stat' => $stat];
    }
}


/**
 * 计算股票后复权价格
 * @param $originPrice 原始行情价格
 * @param $stockQuoteDate 原始行情日期
 * @param $stockExright 行权记录，可能为多次行权的多条记录
 * @param $reat 如果以元为单位的换算比例
 * @return mixed 复权后的价格
 */
function exrightCalculate($originPrice, $stockQuoteDate, $stockExright, $reat = 1)
{
    // 如果没有复权记录，直接返回原价钱
    if (!$stockExright) {
        return $originPrice;
    }

    // // 计算后复权价格
    // 调用类型：1=>分红;2=>赠送;4=>配送;8=>股本变化;16=>非流通股上市;32=>增发新股上市
    // 因存在混合模式，需要用位运算判断
    $exrightPrice = $originPrice;
    foreach ($stockExright as $item) {
        // 如果复权日期比股票行情日期靠后，不进行复权计算并退出
        if (strtotime($item['Exright_Date']) > strtotime($stockQuoteDate)) {
            continue;
        }
        // 复全日期比股票行情日期靠前，进行复权计算
        // 后复权计算公式:复权后价格=复权前价格×(1+流通股份变动比例)-配(新)股价格×流通股份变动比例+现金红利
        if (($item['Exright_Type'] & 4) != 0) { // 位运算判断第三位是否为1
            // 配送
            $changeRate = $item['Exright_Allotment'] / 100;
            $exrightPrice = ($exrightPrice * (1 + $changeRate)
                - $item['Exright_Allotment_Price'] / $reat * $changeRate);
        }
        if (($item['Exright_Type'] & 2) != 0) { // 位运算判断第二位是否为1
            // 赠股
            $changeRate = $item['Exright_Present'] / 100;
            $exrightPrice = $exrightPrice * (1 + $changeRate);
        }
        if (($item['Exright_Type'] & 1) != 0) { // 位运算判断第一位是否为1
            // 分红
            $exrightPrice = $exrightPrice + $item['Exright_Bonus'] / $reat;
        }
    }
    return $exrightPrice;
}

